
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <poll.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdarg.h>
#include <stdbool.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <linux/videodev2.h>



#define BUFFER_COUNT 32
#define V4L2_PLANE_NUM  1
#define CAMERA_INDEX "/dev/video64"

#define ERRSTR strerror(errno)
#define BYE_ON(cond, ...) \
                    do { \
                        if (cond) { \
                            int errsv = errno; \
                            fprintf(stderr, "ERROR(%s:%d) : ", \
                                __FILE__, __LINE__); \
                            errno = errsv; \
                            fprintf(stderr,  __VA_ARGS__); \
                            abort(); \
                        } \
                    } while(0)

void *bufs[BUFFER_COUNT];
static bool is_multiplanar = false;
static unsigned int buf_lengths[BUFFER_COUNT];
static unsigned int num_planes; // 用于存储驱动报告的平面数量


static void v4l2_check_capability(int fd)
{
    struct v4l2_capability caps;
    memset(&caps, 0, sizeof(struct v4l2_capability));

    /* 
        2. `ioctl(VIDIOC_QUERYCAP)` 查询设备能力，确认支持视频采集和流式I/O。
   + 确认它是否是"捕获设备"，因为有些节点是输出设备
   + 确认它是否支持mmap操作，还是仅支持read/write操作 */
    int ret = ioctl(fd, VIDIOC_QUERYCAP, &caps);
    BYE_ON(ret, "VIDIOC_QUERYCAP failed: %s\n", ERRSTR);
    
    is_multiplanar = caps.capabilities & (V4L2_CAP_VIDEO_CAPTURE_MPLANE |
                        V4L2_CAP_VIDEO_M2M_MPLANE |
                        V4L2_CAP_VIDEO_OUTPUT_MPLANE);
    BYE_ON(!is_multiplanar, "错误: 设备不支持多平面(multiplanar)视频采集\n");
    BYE_ON(!(caps.capabilities & V4L2_CAP_STREAMING), "错误: 设备不支持流式(streaming) I/O\n");
}

static void v4l2_enum_format(int fd)
{
    printf("---------- 支持的格式 ----------\n");
    struct v4l2_fmtdesc fmtdesc;
    memset(&fmtdesc, 0, sizeof(fmtdesc));
    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;

    while (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) == 0)
    {
        printf("格式: %s (%.4s)\n", fmtdesc.description, (char*)&fmtdesc.pixelformat);

        struct v4l2_frmsizeenum fsenum;
        memset(&fsenum, 0, sizeof(fsenum));
        fsenum.pixel_format = fmtdesc.pixelformat;

        while (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &fsenum) == 0)
        {
            if (fsenum.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
                printf("  - 分辨率: %dx%d\n", fsenum.discrete.width, fsenum.discrete.height);
            }
            fsenum.index++;
        }
        fmtdesc.index++;
    }
    printf("---------------------------------\n");
}


static void v4l2_set_format(int fd)
{
    struct v4l2_format fmt;
    memset(&fmt, 0, sizeof(struct v4l2_format));
    
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
    
    int ret = ioctl(fd, VIDIOC_G_FMT, &fmt);
    BYE_ON(ret < 0, "VIDIOC_G_FMT failed: %s\n", ERRSTR);
    BYE_ON(fmt.fmt.pix_mp.num_planes != V4L2_PLANE_NUM,
        "plane num is not %d\n", V4L2_PLANE_NUM);
    printf("old format: width = %u, height = %u, 4cc = %.4s\n",
        fmt.fmt.pix.width, fmt.fmt.pix.height,
        (char*)&fmt.fmt.pix.pixelformat);

    if (is_multiplanar)
    {
        fmt.fmt.pix_mp.width = 1920;
        fmt.fmt.pix_mp.height = 1080;
        fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_NV12 ;
        fmt.fmt.pix_mp.field = V4L2_FIELD_ANY;

        for (unsigned i = 0; i < fmt.fmt.pix_mp.num_planes; i++)
            fmt.fmt.pix_mp.plane_fmt[i].bytesperline = 0;
    }
    else
    {
        fmt.fmt.pix_mp.width = 1920;
        fmt.fmt.pix_mp.height = 1080;
        fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_NV12 ;

        fmt.fmt.pix.bytesperline = 0;
    }
    /* 4. `ioctl(VIDIOC_S_FMT)` 设置格式，选择并设置期望的分辨率和像素格式。 */
    ret = ioctl(fd, VIDIOC_S_FMT, &fmt);
    BYE_ON(ret < 0, "VIDIOC_S_FMT failed: %s\n", ERRSTR);

    ret = ioctl(fd, VIDIOC_G_FMT, &fmt);
    BYE_ON(ret < 0, "VIDIOC_G_FMT failed: %s\n", ERRSTR);
    printf("cur format: width = %u, height = %u, 4cc = %.4s\n",
        is_multiplanar ? fmt.fmt.pix_mp.width : fmt.fmt.pix.width,
        is_multiplanar ? fmt.fmt.pix_mp.height : fmt.fmt.pix.height,
        is_multiplanar ? (char*)&fmt.fmt.pix_mp.pixelformat : (char*)&fmt.fmt.pix.pixelformat);

    num_planes = fmt.fmt.pix_mp.num_planes;
    printf("设置后平面数量: %u\n", num_planes);
    BYE_ON(num_planes > VIDEO_MAX_PLANES, "驱动返回的平面数量过多: %d\n", num_planes);
}

static void v4l2_request_buffers(int fd)
{
    struct v4l2_requestbuffers rqbufs;
    memset(&rqbufs, 0, sizeof(struct v4l2_requestbuffers));
    /* 5. `ioctl(VIDIOC_REQBUFS)` 请求缓冲区，向驱动申请若干帧内存，APP可以申请很多个buffer，但是驱动程序不一定能申请到 */
    
    rqbufs.count = BUFFER_COUNT;
    rqbufs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
    rqbufs.memory = V4L2_MEMORY_MMAP;/* FIXME:这几种区别是什么 */
    int ret = ioctl(fd, VIDIOC_REQBUFS, &rqbufs);
    BYE_ON(ret < 0, "VIDIOC_REQBUFS failed: %s\n", ERRSTR);
    BYE_ON(rqbufs.count < BUFFER_COUNT, "video node allocated only "
        "%u of %u buffers\n", rqbufs.count, BUFFER_COUNT);
}

static void v4l2_query_buffers(int fd)
{
    /* 6. `ioctl(VIDIOC_QUERYBUF)` 和 `mmap()`，循环查询每个缓冲区的物理信息，并将其映射到应用的虚拟地址空间。
   * 如果申请到了N个buffer，这个ioctl就应该执行N次
   * 执行mmap后，APP就可以直接读写这些buffer*/

    for (int i = 0; i < BUFFER_COUNT; i++)
    {
        struct v4l2_plane planes[V4L2_PLANE_NUM];
        struct v4l2_buffer buf;
        memset(&planes, 0, sizeof(planes) * V4L2_PLANE_NUM);
        memset(&buf, 0, sizeof(buf));

        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = i;
        buf.m.planes = planes;
        buf.length = num_planes;

        int ret = ioctl(fd, VIDIOC_QUERYBUF, &buf);
        BYE_ON(ret < 0, "VIDIOC_QBUF for buffer %d failed: %s\n", buf.index, ERRSTR);
        
        // mmap通常只需要对第一个平面的偏移进行一次映射，长度为整个缓冲区大小
        buf_lengths[i] = buf.m.planes[0].length;
        bufs[i] = mmap(NULL, buf.m.planes[0].length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.planes[0].m.mem_offset);
        BYE_ON(bufs[i] == MAP_FAILED, "mmap 失败: %s\n", ERRSTR);
    }
}

static void v4l2_qbuf(int fd)
{
    /* 7. `ioctl(VIDIOC_QBUF)` 将缓冲区放入队列，在开始前，把所有缓冲区都“还给”驱动，让其填充数据。如果申请到了N个buffer，这个ioctl就应该执行N次 */
    for (unsigned int i = 0; i < BUFFER_COUNT; ++i)
    {
        struct v4l2_plane planes[V4L2_PLANE_NUM];
        memset(&planes, 0, sizeof(planes) * V4L2_PLANE_NUM);

        struct v4l2_buffer buf;
        memset(&buf, 0, sizeof buf);
        buf.index = i;
        buf.length = V4L2_PLANE_NUM;            
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.m.planes = planes;
        int ret = ioctl(fd, VIDIOC_QBUF, &buf);
        BYE_ON(ret < 0, "VIDIOC_QBUF for buffer %d failed: %s\n",
            buf.index, ERRSTR);
    }
}

/* 
9. 循环采集: 使用poll/select监测buffer，然后从"完成链表"中取出buffer，处理后再放入"空闲链表"
   * poll/select
   * ioctl VIDIOC_DQBUF：从"完成链表"中取出buffer
   * 处理：前面使用mmap映射了每个buffer的地址，处理时就可以直接使用地址来访问buffer
   * ioclt VIDIOC_QBUF：把buffer放入"空闲链表"

10. `ioctl(VIDIOC_STREAMOFF)` 停止采集。*/
int main(void)
{
    int fd;
    struct pollfd fds[1];
    char filename[32];
    int filecnt = 0;
    /* 1. `open()` 打开设备 (`/dev/video64`) */
    fd = open(CAMERA_INDEX, O_RDWR);
    BYE_ON(fd < 0, "failed to open %s: %s\n", CAMERA_INDEX, ERRSTR);
    v4l2_check_capability(fd);
    v4l2_enum_format(fd);
    v4l2_set_format(fd);
    v4l2_request_buffers(fd);
    v4l2_query_buffers(fd);
    v4l2_qbuf(fd);

    /* 8. `ioctl(VIDIOC_STREAMON)` 启动采集，命令硬件开始工作。 */
    int type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
    int ret = ioctl(fd, VIDIOC_STREAMON, &type);
    BYE_ON(ret < 0, "STREAMON failed: %s\n", ERRSTR);

    while (1)
    {
        /* poll */
        memset(fds, 0, sizeof(fds));
        fds[0].fd = fd;
        fds[0].events = POLLIN;
        if (1 == poll(fds, 1, -1))
        {
            /* 把buffer取出队列 */
            struct v4l2_plane planes[V4L2_PLANE_NUM]; // 修改点 1: 为 DQBUF/QBUF 操作准备 planes 数组
            struct v4l2_buffer buf;
            memset(&planes, 0, sizeof(planes)); // 修改点 2: 最好在使用前清零
            memset(&buf, 0, sizeof(struct v4l2_buffer));

            buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
            buf.memory = V4L2_MEMORY_MMAP;
            buf.m.planes = planes; // 修改点 4: 将 planes 数组关联到 buffer
            buf.length = V4L2_PLANE_NUM; // 修改点 5: 告诉驱动 planes 数组的大小
            
            if (0 != ioctl(fd, VIDIOC_DQBUF, &buf))
            {
                perror("Unable to dequeue buffer");
                return -1;
            }
            
            /* 把buffer的数据存为文件 */
            sprintf(filename, "video_raw_data_%04d.nv12", filecnt++); // 修改点 6: 文件后缀改为 .nv12 更准确，因为保存的是裸数据
            int fd_file = open(filename, O_RDWR | O_CREAT, 0666);
            if (fd_file < 0)
            {
                printf("can not create file : %s\n", filename);
            }
            printf("capture to %s\n", filename);
            write(fd_file, bufs[buf.index], buf.m.planes[0].bytesused);
            close(fd_file);

            /* 把buffer放入队列 */
            if (0 != ioctl(fd, VIDIOC_QBUF, &buf))
            {
                perror("Unable to queue buffer");
                return -1;
            }
        }
    }

    if (0 != ioctl(fd, VIDIOC_STREAMOFF, &type))
    {
        perror("Unable to stop capture");
        return -1;
    }
    printf("stop capture ok\n");
    close(fd);

    return 0;
}